package de.softwareforge.published.util;

/*
 * Copyright (C) 2007-2008 Henning P. Schmiedehausen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.AnnotationTypeDoc;
import com.sun.javadoc.AnnotationTypeElementDoc;
import com.sun.javadoc.AnnotationValue;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ParameterizedType;
import com.sun.javadoc.ProgramElementDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.SeeTag;
import com.sun.javadoc.Tag;
import com.sun.javadoc.Type;
import com.sun.javadoc.TypeVariable;
import com.sun.javadoc.WildcardType;

import de.softwareforge.published.doclet.PublishedDoclet;
import de.softwareforge.published.proxy.Invoker;
import de.softwareforge.published.taglet.PublishedTaglet;

import static de.softwareforge.published.util.LocaleMsg.ERROR_NO_STATUS_INFORMATION;
import static de.softwareforge.published.util.LocaleMsg.NOTICE_NEED_RERUN;


/**
 * The StatusCalculator builds a tree of status informations from the root document to all elements.
 *
 * @author <a href="mailto:henning@schmiedehausen.org">Henning P. Schmiedehausen</a>
 * @version $Id:PublishedStatusCalculator.java 357 2007-08-13 20:57:20Z hps@intermeta.de $
 * @api.level unpublished
 */
public class StatusCalculator
{
    private final PublishedDoclet doclet;

    /**
     * Keeps the Published Status for all elements of the Javadocs. Each Element implements Doc, so its published Status can be
     * found here.
     */
    private final Map<Doc, PublishedStatus> publishedStatusMap = new HashMap<Doc, PublishedStatus>();

    /** A map of synthesized Tags for any given Document. It allows us to sneak additional Tags into the Javadocs. */
    private final Map<Doc, Map<String, Tag>> tagMap = new HashMap<Doc, Map<String, Tag>>();

    private boolean needRerun = false;

    /**
     * Creates a new StatusCalculator object.
     *
     * @param doclet The base doclet using this calculator.
     */
    public StatusCalculator(final PublishedDoclet doclet)
    {
        this.doclet = doclet;
    }

    /**
     * This method works downward from the given element to calculate its status and of all its children.
     *
     * @param publishedStatus The parent status object. This must not be kept inside this method, it must be cloned every time it is
     *                        needed!
     * @param document A document element.
     *
     * @throws IllegalStateException When a subclass of {@link Doc} is encountered that is unknown to the status calculator.
     */
    public void buildStatusTree(final PublishedStatus publishedStatus, final Doc document)
    {
        PublishedStatus status = publishedStatusMap.get(document);

        // If we enter here and find a status element without final state set, then
        // this status element has been created by the annotation traversal. Nuke it
        // out, because we provide the real state.
        if (status == null)
        {
            status = publishedStatus.clone(); // Build a copy, we might want to change visibility.
            status.setInProgress(false);
            status.setHasTag(false);

            // Do we have an explicit tag setting for this document?
            Tag[] tags = document.tags(PublishedTaglet.NAME);
            String[] fields = Utils.processTags(tags, doclet.getMessages());

            if (fields != null)
            {
                // Yes, we have.
                Doc holderDocument = tags[0].holder();
                PublishInformation publishInformation =
                    Utils.getPublishInformation(fields, holderDocument.name(), doclet.getMessages());
                status.setPublishInformation(publishInformation);
                status.setHasTag(true);
                // Nothing beats a royal flush.
                status.setFinalState(true);
            }
            publishedStatusMap.put(document, status);
        }

        // If we come here because the element  was referenced by an annotation, then the state will
        // be updated here (again, because this was already set by clone()
        // above. If we traverse the document tree through the package / classes, then finalState will
        // be true and the object collapses into its final state.
        if (!status.isFinalState())
        {
            status.setFinalState(publishedStatus.isFinalState());
            status.setPublishInformation(publishedStatus.getPublishInformation());
        }

        if (status.isFinalState())
        {
            if (!status.hasTag())
            {
                // We have an explicit status information but no tag (yet).
                // Let's add it to the tag list.
                if ((document instanceof RootDoc) && (doclet.getConfiguration().getRootPublishLevel() != null))
                {
                    // Special display element for the Root object that refects the root state.
                    PublishedStatus rootStatus = new PublishedStatus(doclet);
                    rootStatus.setIsReferenced(true);
                    rootStatus.setPublishInformation(doclet.getConfiguration().getRootPublishLevel());
                    rootStatus.setHasTag(true);

                    addTag(new PublishedStatusTag(document, rootStatus));
                }
                else if (status.getPublishInformation() != null)
                {
                    addTag(new PublishedStatusTag(document, status));
                }
                status.setHasTag(true);
            }
        }

        // Avoid endless recursion. This tag is currently processing, skip it.
        // Also skip if we traverse from an annotation (publishedStatus.isFinalState() == false)
        // and we already have a final element.
        //
        // Don't pull this above the final state checks, else our royal flush gets discarded.
        if (status.isInProgress() || (!publishedStatus.isFinalState() && status.isFinalState()))
        {
            return;
        }

        status.setInProgress(true);

        // Walk down the tree with our current status.

        // All Program Elements have annotations.
        if (document instanceof ProgramElementDoc)
        {
            initAnnotations(((ProgramElementDoc) document).annotations());
        }

        if (document instanceof RootDoc)
        {
            for (PackageDoc packageDoc: ((RootDoc) document).specifiedPackages())
            {
                buildStatusTree(status, packageDoc);
            }
        }

        if (document instanceof PackageDoc)
        {

            for (ClassDoc classDoc: ((PackageDoc) document).allClasses(false))
            {
                buildStatusTree(status, classDoc);
            }

            for (ClassDoc annotationTypeDoc: ((PackageDoc) document).annotationTypes())
            {
                buildStatusTree(status, annotationTypeDoc);
            }

            initAnnotations(((PackageDoc) document).annotations());
        }

        if (document instanceof ClassDoc)
        {
            ClassDoc classDoc = (ClassDoc) document;

            for (ConstructorDoc constructorDoc: classDoc.constructors())
            {
                buildStatusTree(status, constructorDoc);
            }

            for (FieldDoc fieldDoc: classDoc.enumConstants())
            {
                buildStatusTree(status, fieldDoc);
            }

            for (FieldDoc fieldDoc: classDoc.fields(false))
            {
                buildStatusTree(status, fieldDoc);
            }

            for (ClassDoc innerClassDoc: classDoc.innerClasses(false))
            {
                buildStatusTree(status, innerClassDoc);
            }

            for (MethodDoc methodDoc: classDoc.methods(false))
            {
                buildStatusTree(status, methodDoc);
            }
        }

        if (document instanceof AnnotationTypeDoc)
        {
            for (AnnotationTypeElementDoc annotationTypeElementDoc: ((AnnotationTypeDoc) document).elements())
            {
                buildStatusTree(status, annotationTypeElementDoc);
            }
        }

        status.setInProgress(false);
    }

    /**
     * Initialize the Annotation Elements. Annotations are a special case, because they can be referenced from everywhere and must
     * have a status object assigned, else the visibility check will fail. However, their actual publishing status is not generated
     * when they are referenced but when the tree traversal hits them as method and/or class element.
     *
     * <p>This method only initializes the status object if none exists.</p>
     *
     * @param annotationDescs An array of {@link AnnotationDesc} objects to format.
     */

    private void initAnnotations(final AnnotationDesc[] annotationDescs)
    {
        // Use a new default element as the root, not the parent element.
        PublishedStatus status = new PublishedStatus(doclet, false);

        for (AnnotationDesc annotationDesc: annotationDescs)
        {
            buildStatusTree(status, annotationDesc.annotationType());

            for (AnnotationDesc.ElementValuePair elementValuePair: annotationDesc.elementValues())
            {
                buildStatusTree(status, elementValuePair.element());
            }
        }
    }

    /**
     * This method works downward from the given element to find the lowest elements and then bring its visibility status back up.
     * If any lower element is visible, this also makes all the elements above it visible. If an element is encountered that has no
     * Status Information assigned, this is an error and flagged.
     *
     * @param document A document element.
     *
     * @return True if this element should be visible in the resulting javadocs.
     *
     * @throws IllegalStateException When an Javadoc element without status information is encountered.
     */
    public boolean buildVisibilityTree(final Doc document)
    {
        PublishedStatus status = publishedStatusMap.get(document);

        if (status == null)
        {
            throw new IllegalStateException(doclet.getMessages().getText(ERROR_NO_STATUS_INFORMATION, document.toString()));
        }

        boolean visible = status.isVisible();

        if (status.isInProgress())
        {
            return visible; // It can get only 'more visible'...
        }

        status.setInProgress(true);

        // Walk down the tree with our current visibility.

        if (document instanceof RootDoc)
        {
            for (PackageDoc packageDoc: ((RootDoc) document).specifiedPackages())
            {
                visible |= buildVisibilityTree(packageDoc);
            }
        }

        if (document instanceof PackageDoc)
        {
            for (ClassDoc classDoc: ((PackageDoc) document).allClasses())
            {
                visible |= buildVisibilityTree(classDoc);
            }

            for (AnnotationDesc annotationDesc: ((PackageDoc) document).annotations())
            {
                visible |= buildVisibilityTree(annotationDesc.annotationType());

                for (AnnotationDesc.ElementValuePair elementValuePair: annotationDesc.elementValues())
                {
                    visible |= buildVisibilityTree(elementValuePair.element());
                }
            }
        }

        if (document instanceof ClassDoc)
        {
            ClassDoc classDoc = (ClassDoc) document;

            for (ConstructorDoc constructorDoc: classDoc.constructors())
            {
                visible |= buildVisibilityTree(constructorDoc);
            }

            for (FieldDoc fieldDoc: classDoc.enumConstants())
            {
                visible |= buildVisibilityTree(fieldDoc);
            }

            for (FieldDoc fieldDoc: classDoc.fields())
            {
                visible |= buildVisibilityTree(fieldDoc);
            }

            for (ClassDoc innerClassDoc: classDoc.innerClasses())
            {
                visible |= buildVisibilityTree(innerClassDoc);
            }

            for (MethodDoc methodDoc: classDoc.methods())
            {
                visible |= buildVisibilityTree(methodDoc);
            }
        }

        if (document instanceof AnnotationTypeDoc)
        {
            for (AnnotationTypeElementDoc annotationTypeElementDoc: ((AnnotationTypeDoc) document).elements())
            {
                visible |= buildVisibilityTree(annotationTypeElementDoc);
            }
        }

        status.setIsReferenced(visible);
        status.setInProgress(false);

        // Now we know whether this element should be visible or not. If it is visible, force all referenced
        // elements to be visible, too and rerun.

        if (status.isVisible())
        {

            // As each of the following docs might make an element somewhere in the Javadoc hierarchy visible, any change here
            // requires
            // a rerun of the visibility calculation.

            // See if there are 'see' tags. Make these also visible, because links point there.
            for (SeeTag seeTag: document.seeTags())
            {
                updateReferenced(seeTag.referencedClass());
                updateReferenced(seeTag.referencedMember());
                updateReferenced(seeTag.referencedPackage());
            }

            if (document instanceof ProgramElementDoc)
            {

                // Make Annotations visible.
                for (AnnotationDesc annotation: ((ProgramElementDoc) document).annotations())
                {
                    updateReferenced(annotation);
                }
            }

            if (document instanceof PackageDoc)
            {
                // Make package Annotations visible.
                for (AnnotationDesc annotation: ((PackageDoc) document).annotations())
                {
                    updateReferenced(annotation);
                }
            }

            if (document instanceof ExecutableMemberDoc)
            {
                ExecutableMemberDoc executableMemberDoc = (ExecutableMemberDoc) document;

                for (Parameter parameter: executableMemberDoc.parameters())
                {

                    // Make parameter Annotations visible.
                    for (AnnotationDesc annotation: parameter.annotations())
                    {
                        updateReferenced(annotation);
                    }

                    // Make the parameter type visible.
                    updateReferencedTypes(parameter.type());
                }

                // Make all thrown exceptions visible
                for (ClassDoc exceptionClassDoc: executableMemberDoc.thrownExceptions())
                {
                    updateReferenced(exceptionClassDoc);
                }
            }

            if (document instanceof ClassDoc)
            {
                ClassDoc classDoc = (ClassDoc) document;

                // A class also makes the following elements visible:

                // - Its superclass if it is hidden (superclass())
                updateReferenced(classDoc.superclass());

                // - A hidden interface if implemented (interfaces())
                for (ClassDoc interfaceClassDoc: classDoc.interfaces())
                {
                    updateReferenced(interfaceClassDoc);
                }

                // - Hidden type parameters
                for (TypeVariable typeVariable: classDoc.typeParameters())
                {
                    updateReferencedTypes(typeVariable);
                }

                // The containing class for inner classes has already been processed when generating the status tree.
            }

            if (document instanceof FieldDoc)
            {
                // A Field makes its type visible.
                updateReferencedTypes(((FieldDoc) document).type());
            }

            if (document instanceof MethodDoc)
            {
                MethodDoc methodDoc = (MethodDoc) document;

                // Make the overridden Method visible. This also makes the overridden Class visible.
                updateReferenced(methodDoc.overriddenMethod());

                // Make the return type visible.
                updateReferencedTypes(methodDoc.returnType());
            }

            if (document instanceof AnnotationTypeDoc)
            {
                // Make the referenced Annotation Type Elements visible.
                for (AnnotationTypeElementDoc annotationTypeElementDoc: ((AnnotationTypeDoc) document).elements())
                {
                    updateReferenced(annotationTypeElementDoc);
                }
            }
        }

        return visible;
    }

    private void updateReferenced(final AnnotationDesc annotationDesc)
    {
        AnnotationTypeDoc annotationTypeDoc = annotationDesc.annotationType();
        updateReferenced(annotationTypeDoc);

        for (AnnotationTypeElementDoc annotationTypeElementDoc: annotationTypeDoc.elements())
        {
            updateReferenced(annotationTypeElementDoc);
            updateReferenced(annotationTypeElementDoc.defaultValue());
        }

        for (AnnotationDesc.ElementValuePair elementValuePair: annotationDesc.elementValues())
        {
            updateReferenced(elementValuePair.element());
            updateReferenced(elementValuePair.element().defaultValue());
            updateReferenced(elementValuePair.value());
        }
    }

    private void updateReferenced(final AnnotationValue annotationValue)
    {
        if (annotationValue != null)
        {
            Object annotationTypeValue = annotationValue.value();

            if (annotationTypeValue instanceof Type)
            {
                updateReferencedTypes((Type) annotationTypeValue);
            }

            if (annotationTypeValue instanceof AnnotationDesc)
            {
                updateReferenced((AnnotationDesc) annotationTypeValue);
            }

            if (annotationTypeValue instanceof Doc)
            {
                updateReferenced((Doc) annotationTypeValue);
            }

            if (annotationTypeValue instanceof AnnotationValue[])
            {
                AnnotationValue[] annotationValues = (AnnotationValue[]) annotationTypeValue;

                for (AnnotationValue value: annotationValues)
                {
                    updateReferenced(value);
                }
            }
        }
    }

    /**
     * Update the visibility of a given Javadoc element.
     *
     * @param document The Javadoc element to use.
     */
    private void updateReferenced(final Doc document)
    {
        if (document != null)
        {
            PublishedStatus publishedStatus = publishedStatusMap.get(document);

            // If this is null, this might be document outside our scope.
            if (publishedStatus != null)
            {

                // Only rerun if this is not already visible.
                if (!publishedStatus.isVisible())
                {
                    publishedStatus.setIsReferenced(true);
                    setNeedRerun(true);
                    doclet.getMessages().notice(NOTICE_NEED_RERUN, document.toString());
                }
            }
        }
    }

    /**
     * Update all Classdocs and elements that are tied to a given Type.
     *
     * @param type The Type to check.
     */
    private void updateReferencedTypes(final Type type)
    {
        if (type != null)
        {
            // Update our classdoc type
            updateReferenced(type.asClassDoc());

            if (type.asParameterizedType() != null)
            {
                ParameterizedType parameterizedType = type.asParameterizedType();

                updateReferencedTypes(parameterizedType.superclassType());

                for (Type interfaceType: parameterizedType.interfaceTypes())
                {
                    updateReferencedTypes(interfaceType);
                }

                for (Type typeArgument: parameterizedType.typeArguments())
                {
                    updateReferencedTypes(typeArgument);
                }
            }

            if (type.asTypeVariable() != null)
            {
                TypeVariable typeVariable = type.asTypeVariable();

                for (Type boundsType: typeVariable.bounds())
                {
                    updateReferencedTypes(boundsType);
                }
            }

            if (type.asWildcardType() != null)
            {
                WildcardType wildcardType = type.asWildcardType();

                for (Type boundsType: wildcardType.extendsBounds())
                {
                    updateReferencedTypes(boundsType);
                }

                for (Type boundsType: wildcardType.superBounds())
                {
                    updateReferencedTypes(boundsType);
                }
            }
        }
    }

    /**
     * Returns the value of the need rerun property.
     *
     * @return The value of the need rerun property.
     */
    public boolean isNeedRerun()
    {
        return needRerun;
    }

    /**
     * Sets the value of the need rerun property.
     *
     * @param needRerun The new need rerun value.
     */
    public void setNeedRerun(final boolean needRerun)
    {
        this.needRerun = needRerun;
    }

    /**
     * Return True if the given document should be visible in the Javadoc output.
     *
     * @param document A Javadoc element that should be checked.
     *
     * @return True if the resulting element should be visible in the Javadoc output.
     */
    public boolean isVisible(final Doc document)
    {
        PublishedStatus status = publishedStatusMap.get(document);

        if (status == null)
        {
            // This is a special case. A Document element came by that
            // has no status entry set. This means that it is not part of this Javadoc
            // package (because everything that is has already been processed in buildStatusTree().

            // Create a new Status on default level.
            status = new PublishedStatus(doclet);
            publishedStatusMap.put(document, status);
        }

        return status.isVisible();
    }

    private void addTag(final Tag tag)
    {
        Doc target = tag.holder();
        Map<String, Tag> tags = tagMap.get(target);

        if (tags == null)
        {
            tags = new HashMap<String, Tag>();
            tagMap.put(target, tags);
        }

        tags.put(tag.name(), tag);
    }

    private Map<String, Tag> getTags(final Doc target)
    {
        Map<String, Tag> tags = tagMap.get(target);

        if (tags == null)
        {
            tags = new HashMap<String, Tag>();
            tagMap.put(target, tags);
        }

        return tags;
    }

    /**
     * An invoker to wrap the tags() method of the various {@link Doc} elements. It allows to sneak additional tags in.
     *
     * @author <a href="mailto:henning@schmiedehausen.org">Henning P. Schmiedehausen</a>
     * @version $Id:PublishedStatusCalculator.java 357 2007-08-13 20:57:20Z hps@intermeta.de $
     */
    public class TagsInvoker
            implements Invoker
    {

        /**
         * Creates a new TagsInvoker object.
         */
        public TagsInvoker()
        { }

        /**
         * @see Invoker#invoke(Object, Method, Object[])
         */
        public Object invoke(final Object target, final Method method, final Object[] args) throws Throwable
        {
            Tag[] results = (Tag[]) method.invoke(target, args);

            return buildTags(target, results, args);
        }

        private Tag[] buildTags(final Object target, final Tag[] originalTags, final Object[] args)
        {

            String key = null;

            if ((args != null) && (args.length > 0))
            {
                key = (String) args[0];

                if (key.charAt(0) != '@')
                {
                    key = "@" + key;
                }
            }

            Map<String, Tag> additionalTags = new HashMap<String, Tag>(getTags((Doc) target));

            List<Tag> returnTags = new ArrayList<Tag>();

            boolean ignoreTags = doclet.getConfiguration().isIgnoreTags();

            for (Tag tag: originalTags)
            {
                if (ignoreTags && PublishedTaglet.NAME.equals(tag.name()))
                {
                    continue;
                }

                if ((key == null) || tag.name().equals(key))
                {
                    returnTags.add(tag);
                }

                if (additionalTags.containsKey(tag.name()))
                {
                    additionalTags.remove(tag.name());
                }
            }

            for (Tag tag: additionalTags.values())
            {
                if (ignoreTags && PublishedTaglet.NAME.equals(tag.name()))
                {
                    continue;
                }

                if ((key == null) || tag.name().equals(key))
                {
                    returnTags.add(tag);
                }
            }

            return returnTags.toArray(new Tag[returnTags.size()]);
        }
    }
}
